﻿using System;
using System.Collections.Generic;
using Notoric.Commanding.Holders;

namespace Notoric.Commanding.CommandBase
{
    /// <summary>
    /// Implementation of the command parameter set.
    /// </summary>
    /// <seealso cref="IEnumerable{T}"/>
    public class ParameterSet : IEnumerable<Parameter>
    {
        #region Public Methods

        /// <summary>
        /// Adds a new parameter to the parameter set of this command.
        /// </summary>
        /// <param name="name">Name of the new parameter</param>
        /// <param name="direction">Direction of the parameter. One of the <see cref="ParameterDirection"/> values</param>
        /// <param name="valueHolder">Reference to the value holder for the new parameter</param>
        /// <exception cref="ParameterExistsException">
        /// If there already is a parameter with the same name in the command parameter set.
        /// </exception>
        /// <exception cref="ArgumentNullException">
        /// The value holder is null.
        /// </exception>
        public void AddParameter(string name, ParameterDirection direction, IValueHolder valueHolder)
        {
            if (parameters.ContainsKey(name))
                throw new ParameterExistsException(String.Format(
                    "A parameter with the same name ({0}) already exists in this command!",
                    name)
                    );

            parameters.Add(name, new Parameter(name, direction, valueHolder));
        }

        /// <summary>
        /// Binds a value of a given parameter of this command to a value of a given parameter on another command.
        /// </summary>
        /// <param name="name">Name of the parameter of this command whose value is being bound to another command</param>
        /// <param name="command">Command to which the value is being bound</param>
        /// <param name="outputName">Name of the parameter of the command to which the value is being bound</param>
        /// <exception cref="ArgumentNullException">
        /// Some of the parameters is null.
        /// </exception>
        /// <exception cref="ParameterDoesNotExistException">
        /// The parameter being bound does not exist on this command or the target parameter does not exist on the other command.
        /// </exception>
        /// <exception cref="IncompatibleTypesException">
        /// The parameters being bound do not have compatible value types.
        /// </exception>
        /// <exception cref="InvalidOperationException">
        /// The parameter beind bound (i.e. the one identified by <paramref name="name"/>) is output.
        /// </exception>
        public void BindParameterToCommand(string name, CommandBase command, string outputName)
        {
            if (name == null)
                throw new ArgumentNullException("name");
            if (command == null)
                throw new ArgumentNullException("command");
            if (outputName == null)
                throw new ArgumentNullException("outputName");

            Parameter myParam = GetParameter(name);
            Parameter hisParam = command.GetParameter(outputName);

            if (myParam.Direction == ParameterDirection.Out)
                throw new InvalidOperationException("An output command parameter cannot be bound to another parameter value!");

            if (!myParam.ValueHolder.ValueType.IsAssignableFrom(hisParam.ValueHolder.ValueType))
                throw new IncompatibleTypesException(String.Format(
                    "Cannot bind the parameter {0} to the parameter {0} of the subcommand. Types of the values are incompatible!",
                    name,
                    outputName)
                    );

            myParam.Command = command;
            myParam.ValueHolder = hisParam.ValueHolder;
        }

        /// <summary>
        /// Returns a reference to the parameter with a given name in the command parameter set.
        /// </summary>
        /// <param name="name">Name of the parameter that is being searched</param>
        /// <returns>A reference to a <see cref="Parameter"/> instance if the parameter is found, null otherwise.</returns>
        public Parameter GetParameter(string name)
        {
            Parameter param;

            if (!parameters.TryGetValue(name, out param))
                param = null;

            return param;
        }

        /// <summary>
        /// Gets the value of a given parameter.
        /// </summary>
        /// <typeparam name="T">Type of the parameter value</typeparam>
        /// <param name="name">Nama of the parameter whose value is being searched</param>
        /// <returns>Value of the parameter</returns>
        /// <exception cref="ParameterDoesNotExistException">
        /// The parameter does not exist.
        /// </exception>
        /// <exception cref="InvalidCastException">
        /// The parameter value has a different type.
        /// </exception>
        public T GetParameterValue<T>(string name)
        {
            Parameter param;

            if (!parameters.TryGetValue(name, out param))
                throw new ParameterDoesNotExistException(String.Format(
                    "The command does not contain any parameter named {0}!",
                    name)
                    );

            if (!typeof(T).IsAssignableFrom(param.ValueHolder.ValueType))
                throw new InvalidCastException(
                    String.Format(
                        "Cannot assign the value of type {0} to type {1}!",
                        param.ValueHolder.ValueType.Name,
                        typeof(T).Name
                        )
                    );

            return (T)param.ValueHolder.WrappedValue;
        }

        /// <summary>
        /// Gets the value of a given parameter as a reference to <see cref="object"/>.
        /// </summary>
        /// <param name="name">Name of the parameter whose value is being searched</param>
        /// <returns>Value of the parameter</returns>
        /// <exception cref="ParameterDoesNotExistException">
        /// Parameter with the given name does not exist.
        /// </exception>
        /// <exception cref="ValueNotKnownException">
        /// The value of the parameter is not known (i.e. the state of the value is <see cref="ValueState.Unknown"/>).
        /// </exception>
        public object GetUntypedParameterValue(string name)
        {
            Parameter param;

            if (!parameters.TryGetValue(name, out param))
                throw new ParameterDoesNotExistException(String.Format(
                    "The command does not contain any parameter named {0}!",
                    name)
                    );

            return param.ValueHolder.WrappedValue;
        }

        /// <summary>
        /// Sets the value of a parameter.
        /// </summary>
        /// <typeparam name="T">Type of the parameter value</typeparam>
        /// <param name="name">Name of the parameter whose value is being set</param>
        /// <param name="value">New value of the parameter</param>
        /// <exception cref="ParameterDoesNotExistException">
        /// Parameter with the given name does not exist.
        /// </exception>
        /// <exception cref="InvalidCastException">
        /// The parameter value does not have the same type as the value that was passed.
        /// </exception>
        public void SetParameterValue<T>(string name, T value)
        {
            Parameter param;

            if (!parameters.TryGetValue(name, out param))
                throw new ParameterDoesNotExistException(String.Format(
                    "The command does not contain any parameter named {0}!",
                    name)
                    );

            if (!param.ValueHolder.ValueType.IsAssignableFrom(typeof(T)))
                throw new InvalidCastException(
                    String.Format(
                        "Cannot assign a value of type {0} to type {1}!",
                        typeof(T).Name,
                        param.ValueHolder.ValueType.Name
                        )
                    );

            param.ValueHolder.WrappedValue = value;
        }

        #endregion

        #region IEnumerable<Parameter> Members

        /// <summary>
        /// Returns an enumerator that iterates through the collection.
        /// </summary>
        /// <returns>A <see cref="IEnumerator{T}"/> that can be used to iterate through the collection.</returns>
        public IEnumerator<Parameter> GetEnumerator()
        {
            return parameters.Values.GetEnumerator();
        }

        #endregion

        #region IEnumerable Members

        /// <summary>
        /// Returns an enumerator that iterates through the collection.
        /// </summary>
        /// <returns>A <see cref="System.Collections.IEnumerator"/> that can be used to iterate through the collection.</returns>
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return parameters.Values.GetEnumerator();
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Internal dictionary mapping the names of the parameters to their instances.
        /// </summary>
        private Dictionary<string, Parameter> parameters = new Dictionary<string, Parameter>();

        #endregion
    }
}
